package com.tjc.spring.core.resource;

import com.tjc.spring.core.*;
import com.tjc.spring.core.exception.LoadResourceException;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.IOException;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @Author: tjc
 * @description
 * @Date Created in 22:30 2021-12-21
 */
public class XmlBeanDefinitionReader {
    private static final Logger LOG = Logger.getLogger(XmlBeanDefinitionReader.class.getName());

    static final String BEAN_ELE = "bean";
    static final String PROPERTY_ELE = "property";
    static final String ID_ATTR = "id";
    static final String CLASS_ATTR = "class";
    static final String NAME_ATTR = "name";
    static final String VALUE_ATTR = "value";
    static final String REF_ATTR = "ref";

    private static final String CONSTRUCTOR_ARG_ELE = "constructor-arg";

    BeanDefinitionRegistry registry;

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        this.registry = registry;
    }

    public void loadDefinitions(Resource resource) {
        try {
            SAXReader reader = new SAXReader();
            Document document = reader.read(resource.getInputStream());
            Element beans = document.getRootElement();
            for (Element element : beans.elements(BEAN_ELE)) {
                checkBeanAttr(element);
                String id = element.attribute(ID_ATTR).getValue();
                String className = element.attribute(CLASS_ATTR).getValue();
                GenericBeanDefinition bd = null;
                try {
                    Class<?> clazz = Class.forName(className);
                    bd = new GenericBeanDefinition(id, className, clazz);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    System.exit(1);
                }
                parsePropertyElements(element, bd);
                parseConstructArgElements(element, bd);
                registry.register(bd.getID(), bd);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    private void parseConstructArgElements(Element beanEle, GenericBeanDefinition bd) {
        Iterator<Element> iter = beanEle.elementIterator(CONSTRUCTOR_ARG_ELE);
        ConstructArgument constructArg = new ConstructArgument();
        while (iter.hasNext()) {
            Element ele = iter.next();
            constructArg.addValueHandler(parseConstructArgElement(ele, bd));
        }
        bd.setConstructArgument(constructArg);
    }

    private ConstructArgument.ValueHolder parseConstructArgElement(Element ele, GenericBeanDefinition bd) {
        Object value = parsePropertyValue(ele, bd, null);

        return new ConstructArgument.ValueHolder(value, null, null);
    }

    private void parsePropertyElements(Element beanEle, GenericBeanDefinition bd) {
        Iterator<Element> iter = beanEle.elementIterator(PROPERTY_ELE);
        while (iter.hasNext()) {
            Element element = iter.next();
            parsePropertyElement(element, bd);
        }
    }

    private void parsePropertyElement(Element ele, GenericBeanDefinition bd) {
        checkPropertyAttr(ele);
        String propName = ele.attributeValue(NAME_ATTR);
        Object value = parsePropertyValue(ele, bd, propName);
        bd.addProperty(new Property(propName, value));
    }

    private Object parsePropertyValue(Element ele, GenericBeanDefinition bd, String propertyName) {
        String elementName = (propertyName != null) ? "<property> element for property '" + propertyName + "'" : "<constructor-arg> element";
        boolean hasRefAttribute = (ele.attribute(REF_ATTR) != null);
        boolean hasValueAttribute = (ele.attribute(VALUE_ATTR) != null);
        if (hasValueAttribute) {
            TypedStringValue value = new TypedStringValue(ele.attributeValue(VALUE_ATTR));
            return value;
        } else if (hasRefAttribute) {
            String refName = ele.attributeValue(REF_ATTR);
            if (StringUtils.isBlank(refName)) {
                // error
                LOG.log(Level.WARNING, elementName + "contains empty 'ref' attribute");
            }
            RuntimeBeanReference ref = new RuntimeBeanReference(refName);
            return ref;
        } else {
            throw new RuntimeException(elementName + "must specify a ref or value");
        }
    }

    private void checkPropertyAttr(Element ele) {
        if (ele.getName() != PROPERTY_ELE) {
            throw new IllegalArgumentException("arg is a property element");
        }
        checkProptyRequireAttr(ele);
    }

    private void checkProptyRequireAttr(Element ele) {
        Attribute idAttr = ele.attribute(NAME_ATTR);
        if (idAttr == null) {
            throw new LoadResourceException("<property> id is required.");
        }
    }

    private static void checkBeanAttr(Element ele) {
        if (ele.getName() != BEAN_ELE) {
            throw new IllegalArgumentException("arg is a bean element");
        }
        checkBeanRequireAttr(ele);
    }

    private static void checkBeanRequireAttr(Element ele) {
        Attribute idAttr = ele.attribute(ID_ATTR);
        Attribute classAttr = ele.attribute(CLASS_ATTR);
        if (idAttr == null) {
            throw new LoadResourceException("<bean> id is required.");
        }
        if (classAttr == null) {
            throw new LoadResourceException("<bean> class is required.");
        }
    }
}
